#############################################
package main;

use vars qw{%attr %defs $readingFnAttributes};
use strict;
use warnings;
use Color;
use Blocking;
use DevIo;
use SetExtensions;
use Data::Dumper;
use Time::HiRes qw(time gettimeofday tv_interval usleep);


#####################################

my %gets = (
	"rgb"          => 0,
	"RGB"          => 0,
	"pct"          => 0,
	"devStateIcon" => 0,
);

# commands, which are returned to webinterface
my %sets = (
	"channels" => 0,
	"levels" => 0,	
	"on:noArg" => 0,
	"off:noArg" => 0,
	"next:noArg" => 0,
	"prev:noArg" => 0,
	"toggle:noArg" => 0,
	"rgb:colorpicker,RGB" => 0,
	"randomize:noArg" => 0	
);

# list of valid commands
my %setStrings = (
	"channels" => 0,
	"levels" => 0,	
	"on" => 0,
	"off" => 0,
	"next" => 0,
	"prev" => 0,
	"toggle" => 0,
	"rgb" => 0,
	"randomize" => 0	
);


my %defaults = (
	"webCmd"		=> "rgb:toggle:on:off:next:prev",
	"dimDuration" 	=> 0.5,
	"frequency" 	=> 50
);


my @chState = (chr(0)) x 512;

my $ARTNET_hasJSON = 1;

##############################
sub ARTNET_Initialize($) 
{
	my ($hash) = @_;


	# Provider
	$hash->{ReadFn}  = "ARTNET_Read";
	$hash->{WriteFn}  = "ARTNET_Write";
  	$hash->{Clients} = ":ARTNET_FIXTURE:";

	$hash->{DefFn} = "ARTNET_Define";
	$hash->{NotifyFn} = "ARTNET_Notify";	
	$hash->{UndefFn} = "ARTNET_Undefine";	
	$hash->{GetFn} = "ARTNET_Get";
	$hash->{SetFn} = "ARTNET_Set";
	$hash->{ParseFn} = "ARTNET_Parse";

#	$hash->{AttrList} = "frequency:slider,1,1,100
#						dimDuration
#						loglevel:0,1,2,3,4,5 
#						$readingFnAttributes";
	$hash->{AttrList} = "frequency
						dimDuration
						loglevel 
						$readingFnAttributes";						


	FHEM_colorpickerInit();
	
	eval "use JSON";
	$ARTNET_hasJSON = 0 if($@);	

}

##############################
sub ARTNET_Notify($$)
{
	my ($hash, $dev) = @_;
 	
 	Log3 $hash, 5, "Notify from: " . $dev->{NAME} . " " . @{$dev->{CHANGED}}; 
 	
 	return if($dev->{NAME} ne "global");

 	if( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) {
    	ARTNET_Save();
 	} 	  	
}

##############################
sub ARTNET_Define($$) {
	my ( $hash, $def ) = @_;
	
	my @arg = split("[ \t][ \t]*", $def);

	if (@arg != 4) {
    	my $msg = "wrong syntax: define <name> ARTNET ip[:port] comma_separated_channels";
    	Log 2, $msg;
    	return $msg;
	}

	if (split(":", $arg[2]) == 1) {
		$hash->{DeviceName} = $arg[2] . ":6454";
	} else {
		$hash->{DeviceName} = $arg[2];
	}
	$hash->{STATE} = "off";
	my @channels = split(",", $arg[3]);
	$hash->{channels} = \@channels;

	$hash->{Clients} = ":ARTNET_FIXTURE:";

	my %presets;
  	$hash->{PRESETS} = \%presets;
  	ARTNET_Load($hash);

	Log3 $hash, 3, "ARTNET device defined on IP: " . $hash->{DeviceName} . " with channels: " . join(",", @{$hash->{channels}});

}

#####################################
sub ARTNET_Get($@) {
	my ( $hash, @a ) = @_;

	return "\"set CUL\" needs at least one parameter" if ( @a < 2 );
	return "Unknown argument $a[1], choose one of "
	  . join( " ", sort keys %gets )
	  if ( !defined( $sets{ $a[1] } ) );
}


##############################
sub ARTNET_Set($@) {
	my ( $hash, @a ) = @_;


	return "\"set ARTNET\" needs at least one parameter" if ( @a < 2 );

	if (!defined($setStrings{$a[1]}) && substr($a[1], 0, 7) ne "preset_" && $a[1] ne "rgb" && substr($a[1], 0, 5) ne "Chan_") {
		my %setOptions = %sets;
		foreach (@{$hash->{channels}}) {		
			$setOptions{ "Chan_" . sprintf("%.3d", $_) . ":slider,0,1,255" } = 0;
		}
		#Log3 $hash->{NAME}, 3, "DEBUG: before_load " . Dumper($hash->{PRESETS}); 	
		$setOptions{ "preset_save" } = 0;
		$setOptions{ "preset_load:" . join(",", sort keys %{$hash->{PRESETS}}) } = 0;	 
		$setOptions{ "preset_remove:" . join(",", sort keys %{$hash->{PRESETS}}) } = 0;

		return "Unknown argument $a[1], choose one of " . join(" ", sort keys %setOptions) ;		
	}

	  
	if (defined($hash->{lastCmd}) && time - $hash->{lastCmd} < 0.5) {
		return undef;
	}	  
	  

	my @origChanState = @chState;	  
	  
    # get argument
	my $arg = $a[1];


	my $state = $hash->{STATE};

	my $scene = ReadingsVal($hash->{NAME}, "Preset", 0);

	if (($arg eq "next" || $arg eq "prev") && $state eq "off" && $scene) {
		$arg = "on";
	} elsif ($arg eq "next" || $arg eq "prev") {
		my @scenePresets = sort keys %{$hash->{PRESETS}};

		my $nextScene = $scenePresets[0];
		my $firstScene = $nextScene;
		my $prevScene = $scenePresets[-1];
		
		my $scene = ReadingsVal($hash->{NAME}, "Preset", $nextScene);
		
		my $stop = -1;
		foreach (@scenePresets) {
			if ($stop == 1) {
				$nextScene = $_;
				last;
			}
			if ($scene eq $_) {
				if ($arg eq "next") {
					$stop = 1;
				} elsif ($arg eq "prev") {
					$nextScene = $prevScene;
					last;
				}
			}
			
			$prevScene = $_;
		}
		
		$arg = "preset_load";
		$a[2] = $nextScene;
		#Log3 $hash, 3, "Scene " . $a[2];
	} elsif (substr($arg, 0, 5) eq "Chan_") {
		$a[2] = (split "_", $arg)[1] . ":" . $a[2];				
		$arg = "channels";
	} elsif ($arg eq "randomize") {
		my @data;
		foreach (@{$hash->{channels}}) {
			push(@data, int(rand(256)));			
		}
		
		$arg = "levels";
		$a[2] = join(",", @data);
	}


	if ($arg eq "preset_load") {
		my $scene = $a[2];

		$arg = "levels";
		
		if (exists($hash->{PRESETS}{$scene})) {
			$a[2] = $hash->{PRESETS}{$scene};

			readingsSingleUpdate($hash, "Preset", $scene, undef);			
		}		
	} elsif ($arg eq "rgb") {
		$arg = "levels";
		my @rgb = ( $a[2] =~ m/../g ); # split string every two characters
		
		my @newRgb;		
		foreach (@rgb) {
			push(@newRgb, hex($_));			
		}
		$a[2] = join(",", @newRgb);
	}


	if ($arg eq "toggle" || $arg eq "on") {
		if ($state eq "off") {
			$arg = "on";
		} else {
			$arg = "off";
		}
	}

  	if ($arg eq "off") {
  		# We change state to OFF, leaving READINGS as they are - blackout
		foreach (@{$hash->{channels}}) {
			$chState[$_] = chr(0);
		}
		$state = "off";
		
  	} elsif ($arg eq "on") {
		# We load previous values from READINGS
  		foreach (@{$hash->{channels}}) { 			
  			$chState[$_] = chr(ReadingsVal($hash->{NAME}, "Chan_" . sprintf("%.3d", $_), 0));
  		}
  		
  		$state = ARTNET_UpdateChannelState($hash);
  		
  	} elsif ($arg eq "levels" || $arg eq "channels") {

		if ($arg eq "levels") {
	  		my @chVals;
	      	@chVals = split(',', $a[2]) if (int(@a) == 3);

      	
      		my $i = 0;
			foreach (@chVals) {								
				$chState[@{$hash->{channels}}[$i++]] = chr($_);   															
			}
		}  		

		if ($arg eq "channels") {
	  		my @chVals;
	      	@chVals = split(',', $a[2]) if (int(@a) == 3);
	      	
			foreach (@chVals) {
				my @chLevels = split(':', $_);
				$chState[$chLevels[0]] = chr($chLevels[1]);																	
			}	      		      				
		}
		
      	$state = ARTNET_UpdateChannelState($hash);
  	} elsif ($arg eq "preset_save") {
  		my @curState;
		foreach (@{$hash->{channels}}) {
			push(@curState, ord($chState[$_]));
		}  		
  		$hash->{PRESETS}{$a[2]} = join(",", @curState);
  	} elsif ($arg eq "preset_remove") {
  		delete($hash->{PRESETS}{$a[2]});
  	}

	$hash->{STATE} = $state;
	$hash->{lastCmd} = time;

	if (exists($hash->{helper}{RUNNING_PID})) {
		Log3 $hash, 3, $hash->{NAME} . " dim process is still running.";
	}

	my $socket = new IO::Socket::INET(
		PeerAddr  => $hash->{DeviceName}, 
		Proto => 'udp', 
		LocalPort => 6454) || Log3 $hash, 1, "Error opening socket: $!";

		
	my %dimData;
	$dimData{start} = gettimeofday();
	$dimData{socket} = $socket;		
	$dimData{oldState} = \@origChanState;
	$dimData{newState} = \@chState;
	$dimData{channels} = \@{$hash->{channels}};
	$dimData{duration} = AttrVal($hash->{NAME}, "dimDuration", $defaults{dimDuration});
	$dimData{frequency} = AttrVal($hash->{NAME}, "frequency", $defaults{frequency});
	$dimData{name} = $hash->{NAME};
		

	$hash->{helper}{RUNNING_PID} = BlockingCall("ARTNET_Dim", \%dimData, "ARTNET_DimDone", 10, "ARTNET_DimAborted", $hash) unless(exists($hash->{helper}{RUNNING_PID}));
      	
	return undef;	  
}

################################
sub
ARTNET_DimDone($)
{
	my ($name) = @_;
	
  	return unless(defined($name));

	my $hash = $defs{$name};	
	delete($hash->{helper}{RUNNING_PID});
}
################################
sub
ARTNET_DimAborted($)
{
	my ($hash) = @_;
	
	RemoveInternalTimer($hash);	
	delete($hash->{helper}{RUNNING_PID});
	Log3 $hash, 3, "BlockingCall for ".$hash->{NAME}." was aborted";
}
################################  
sub
ARTNET_Dim($)
{
	my ($dimData) = @_;

	my $name = $dimData->{name};
	my $start = $dimData->{start};
	my $socket = $dimData->{socket};
	my $duration = $dimData->{duration};
	my $frequency = $dimData->{frequency};
	my @channels = @{$dimData->{channels}};	
	my @oldState = @{$dimData->{oldState}};
	my @newState = @{$dimData->{newState}};
	
	$frequency = 50 if ($frequency < 10 || $frequency > 100);
	
		
	my $curTime = [gettimeofday()];
	my $closeCon = 1;
	my @state = @oldState;
	$start = $curTime;	
	
	my $step = 0;
	
	my $diff = 0; 
	
	while ($diff <= $duration) {
		Log3 undef, 5, "in loop " . $diff . " do " . $duration;	
		my $step = $diff / $duration;
		my $level = 0;
		
		foreach (@channels) {
			if ($oldState[$_] ne $newState[$_]) {
				$level = ord($oldState[$_]) + ( ord($newState[$_])-ord($oldState[$_]) ) * $step;
				Log3 undef, 5, "old: " . ord($oldState[$_]) . " new: " . ord($newState[$_]) . " level: " . int($level) . " step: " . $step  . " ch: " . $_;
				$level = 255 if ($level > 255 || $level < 0);
				$state[$_] = chr(int($level + 0.5)); 
			}
		}
				
		my $universe = 0;	
		my $data = "Art-Net\x00\x00\x50\000\016\x00\x01" . chr($universe) . "\x00" . chr(2) . chr(0) . join("", @state);
				
		$socket->send($data);		
		
		my $td = tv_interval($curTime);
		my $sleep = (1000000/$frequency) - $td;
		Log 5, "sleep: " . $sleep . " " . (1000000/25);
		if ($sleep > 0) {
			usleep($sleep);
		}
		$curTime = [gettimeofday()];
		$diff = tv_interval($start, $curTime);			
	}
	

	my $universe = 0;	
	my $data = "Art-Net\x00\x00\x50\000\016\x00\x01" . chr($universe) . "\x00" . chr(2) . chr(0) . join("", @newState);
				
	$socket->send($data);

	if ($closeCon) {		
  		shutdown($socket, 2) if $socket;
   		close($socket) if $socket;
	}
			
	return $name;	
}

################################
sub
ARTNET_UpdateChannelState($)
{
	my ($hash) = @_;
	my $state = "";
	my $msg = "";
	my $isOn = 0;
	#$state = "off";
		
	my $i = 0;
	foreach (@{$hash->{channels}}) {
		#$state = "on" if (ord($chState[$_]) > 0);
		$state .= sprintf("%.2X", ord($chState[$_]));
		$msg .= $_ . ":" . $chState[$_] . ",";
		if (ord($chState[$_]) > 0) {
			$isOn = 1;
		} 			
		readingsSingleUpdate($hash, "Chan_" . sprintf('%.3d', $_), sprintf('%.3d', ord($chState[$_])), 1);			
	}
	
	if ($isOn == 0) {
		$state = "off";
	}
	
	my %addvals;

	if (defined($modules{ARTNET_FIXTURE}{defptr})) {		
		foreach (keys $modules{ARTNET_FIXTURE}{defptr}) {
			Dispatch($hash, $_ . '.' . $msg, \%addvals); # we send message to our logical devices		
		}
	}
	
	return $state;	
}
################################
sub 
ARTNET_Parse($$) {
	my ($hash, $msg) = @_;	
}



################################
sub 
ARTNET_Undefine($$)
{
  my ($hash, $arg) = @_;

  ARTNET_Save();

  return undef;
}
################################
sub
ARTNET_Load($)
{
  my ($hash) = @_;

  return "No statefile specified" if(!$attr{global}{statefile});
  my $statefile = ARTNET_StateFile();
  
  Log3 $hash, 3, "ARTNET_Load statefile: " . $statefile; 

  if (open(FH, "<$statefile")) {
    my $encoded;
    while (my $line = <FH>) {
      chomp $line;
      next if($line =~ m/^#.*$/);
      $encoded .= $line;
    }
    close(FH);

    return if (!defined($encoded));

    Log3 $hash, 3, "LOAD has json: " . $ARTNET_hasJSON;
    Log3 $hash, 5, "Save file content: " . $encoded;

    my $decoded;
    if ($ARTNET_hasJSON) {
    	$decoded = decode_json($encoded);
    } else {
    	$decoded = eval $encoded;
    } 
    Log3 $hash, 3, "LOAD: " . $decoded;
    $hash->{PRESETS} = $decoded->{$hash->{NAME}} if (defined($decoded->{$hash->{NAME}}));
  } else {
    Log3 $hash, 1, "ARTNET_Load: Cannot open $statefile: $!";
  }
  
  return undef;
}
##################################
my $ARTNET_LastSaveTime="";
sub
ARTNET_Save($)
{
	my ($hash) = @_; 
	
	Log3 $hash, 3, "Saving statefile";
	my $time_now = TimeNow();
	return if( $time_now eq $ARTNET_LastSaveTime);
	$ARTNET_LastSaveTime = $time_now;

	if(!$attr{global}{statefile}) {
		Log3 $hash, 3, "No statefile specified";
		return "No statefile specified"; 
	}
	my $statefile = ARTNET_StateFile();

	my $shash;
	for my $d (keys %defs) {
		next if ($defs{$d}{TYPE} ne "ARTNET");
		next if (!defined($defs{$d}{PRESETS}));

		$shash->{$d} = $defs{$d}{PRESETS} if (keys(%{$defs{$d}{PRESETS}}));
	}

	if (open(FH, ">$statefile")) {
    	my $t = localtime;
    	print FH "#$t\n";

    	if ($ARTNET_hasJSON) {
			print FH encode_json($hash) if( defined($hash) );
    	} else {
			my $dumper = Data::Dumper->new([]);
			$dumper->Terse(1);

			$dumper->Values([$hash]);
			print FH $dumper->Dump;
		}

    	close(FH);
	} else {

    	Log3 $hash, 1, "ARTNET_Save: Cannot open $statefile: $!";
  	}

	return undef;
}
##################################
sub
ARTNET_StateFile()
{
  my $statefile = $attr{global}{statefile};
  $statefile = substr $statefile, 0, rindex($statefile, '/')+1;
  return $statefile ."ARTNET.save" if ($ARTNET_hasJSON);  
  return $statefile . "ARTNET.dd.save";
}
#####################################
sub
ARTNET_Read()
{
	Log 1, "Read " . @_;	
}
###############################################
sub
ARTNET_Write()
{
	Log 1, "Write " . @_;
}

###############################################



1;

=pod
=begin html

<a name="FRM_RGB"></a>
<h3>FRM_RGB</h3>
<ul>
allows to drive LED-controllers and other multichannel-devices that use PWM as input by an <a href="http://www.arduino.cc">Arduino</a> running <a href="http://www.firmata.org">Firmata</a>
<br>
The value set will be output by the specified pins as pulse-width-modulated signals.<br>
Requires a defined <a href="#FRM">FRM</a>-device to work.<br><br>
<a name="FRM_RGBdefine"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; FRM_RGB &lt;pin&gt; &lt;pin&gt; &lt;pin&gt; [pin...]</code> <br>
Defines the FRM_RGB device. &lt;pin&gt> are the arduino-pin to use.<br>
For rgb-controlled devices first pin drives red, second pin green and third pin blue.
</ul>
<br>
<a name="FRM_RGBset"></a>
<b>Set</b><br>
<ul>
<code>set &lt;name&gt; on</code><br>
sets the pulse-width of all configured pins to 100%</ul><br>
<ul>
<code>set &lt;name&gt; off</code><br>
sets the pulse-width of all configured pins to 0%</ul><br>
<ul>
<a href="#setExtensions">set extensions</a> are supported</ul><br>
<ul>
<code>set &lt;name&gt; toggle</code><br>
toggles in between the last dimmed value, 0% and 100%. If no dimmed value was set before defaults to pulsewidth 50% on all channels</ul><br>
<ul>
<code>set &lt;name&gt; rgb &lt;value&gt;</code><br>
sets the pulse-width of all channels at once. Also sets the value toggle can switch to<br>
Value is encoded as hex-string, 2-digigs per channel (e.g. FFFFFF for reguler rgb)</ul><br>
<ul>
<code>set &lt;name&gt; pct &lt;value&gt;</code><br>
dims all channels at once while leving the ratio in between the channels unaltered.<br>
Range is 0-100 ('pct' stands for 'percent')</ul><br>
<ul>
<code>set &lt;name&gt; dimUp</code><br>
dims up by 10%</ul><br>
<ul>
<code>set &lt;name&gt; dimDown</code><br>
dims down by 10%</ul><br>
<a name="FRM_RGBget"></a>
<b>Get</b><br>
<ul>
<code>get &lt;name&gt; rgb</code><br>
returns the values set for all channels. Format is hex, 2 nybbles per channel.
</ul><br>
<ul>
<code>get &lt;name&gt; RGB</code><br>
returns the values set for all channels in normalized format. Format is hex, 2 nybbles per channel.
Values are scaled such that the channel with the highest value is set to FF. The real values are calculated
by multipying each byte with the value of 'pct'.
</ul><br>
<ul>
<code>get &lt;name&gt; pct</code><br>
returns the value of the channel with the highest value scaled to the range of 0-100 (percent).
</ul><br>
<a name="FRM_RGBattr"></a>
<b>Attributes</b><br>
<ul>
<li>restoreOnStartup &lt;on|off&gt;</li>
<li>restoreOnReconnect &lt;on|off&gt;</li>
<li><a href="#IODev">IODev</a><br>
Specify which <a href="#FRM">FRM</a> to use. (Optional, only required if there is more
than one FRM-device defined.)
</li>
<li><a href="#eventMap">eventMap</a><br></li>
<li><a href="#readingFnAttributes">readingFnAttributes</a><br></li>
</ul>
</ul>
<br>

=end html
=cut